redmineRパッケージはGitHubからインストールするためにdevtoolsやremotesというパッケージを事前にインストールしておく必要があります。第1回勉強会にて環境構築した方はdevtoolsパッケージがインストールされているハズですので、これを使います。
devtools::install_github("openanalytics/redmineR")
redmineRを読み込めればOK
library(redmineR)
あとはtidyverseを読み込む。tidyverseのうち使うのはこの3つ。
| パッケージ | 説明 |
|---|---|
dplyr |
データフレーム操作 |
lubridate |
日付操作 |
ggplot2 |
可視化 |
lubridateパッケージはtidyverseファミリーですがtidyverseパッケージを読み込んだだけでは読み込まれないないので、別途、明示的に読み込んでください。
require(psych)
require(lubridate)
library(tidyverse) # tidyverseは最後に呼び出す方がコンフリクトの影響小
なお、追加パッケージを読み込んだ場合は、tidyverse::tidyverse_conflicts関数でコンフリクト状態を把握しておくと意図しない動きの場合の原因追及に使えるので便利です。
tidyverse::tidyverse_conflicts()
## -- Conflicts ------------------------------------------------------------- tidyverse_conflicts() --
## x ggplot2::%+%() masks psych::%+%()
## x ggplot2::alpha() masks psych::alpha()
## x lubridate::as.difftime() masks base::as.difftime()
## x lubridate::date() masks base::date()
## x dplyr::filter() masks stats::filter()
## x lubridate::intersect() masks base::intersect()
## x dplyr::lag() masks stats::lag()
## x lubridate::setdiff() masks base::setdiff()
## x lubridate::union() masks base::union()
URLとAPIアクセストークンを環境変数に指定します。
APIアクセストークンはRedmineにログインして、個人設定ページから入手してください。
Sys.setenv("REDMINE_URL" = "http://xxx.xxx.xxx/xxx")
Sys.setenv("REDMINE_TOKEN" = "APIACCESSTOKEN")
プロジェクトの一覧取得とかが軽めのAPIでおすすめ。
redmineR::redmine_projects() %>% as.data.frame()
redmine_issuesでチケット一覧を取得します。
デフォルトの引数だとクローズしたチケットを取得して来れません。
全てのステータスのチケットを取得するためにはstatus_id = "*"を指定します。
issues <- redmineR::redmine_issues(status_id = "*")
皆様はサンプルデータをダウンロードして、読込んでお使いください。
load("../data/redmine_issues_all.RData")
issues %>% head(20) %>% as.data.frame()
<list [2]>やら、NAやら、結構汚いデータなので、きれいにするところから始めます。
issues %>% head(100) %>%
purrr::map_df(class)
projectやtrackerなど、リストから選ぶ系のフィールドがlist型になってしまっています。 list(id="ID", name="NAME")からname="NAME"を取り出したい。
issues %>% head() %>% dplyr::as_data_frame() %>%
dplyr::select(id, project, status) %>% knitr::kable()
| id | project | status |
|---|---|---|
| 29767 | list(id = 1, name = “Redmine”) | list(id = 1, name = “New”) |
| 29764 | list(id = 1, name = “Redmine”) | list(id = 1, name = “New”) |
| 29763 | list(id = 1, name = “Redmine”) | list(id = 5, name = “Closed”) |
| 29758 | list(id = 1, name = “Redmine”) | list(id = 1, name = “New”) |
| 29757 | list(id = 1, name = “Redmine”) | list(id = 1, name = “New”) |
| 29756 | list(id = 1, name = “Redmine”) | list(id = 1, name = “New”) |
日付、日時も文字列型になってしまっています。日付はdateに、日時はPOSIXct型に変換したい。
issues %>% head(20) %>%
dplyr::select(id, created_on, closed_on, updated_on, start_date, due_date) %>%
as_data_frame()
NA有無の確認厄介なのは、リスト型のカラムの中にあるNAです。(categoryとかassigned_toとか)
# NAの有無確認
issues %>% head(100) %>%
purrr::map_df(anyNA)
リスト型変数は任意の型で任意の数のデータを任意な数だけ持てる柔軟性のあるデータ構造です。リスト型の中にリスト型をネストさせることも可能です。
list(list(id = c(1, 2)), list(name = c("Defects")))
## [[1]]
## [[1]]$id
## [1] 1 2
##
##
## [[2]]
## [[2]]$name
## [1] "Defects"
projectの場合、こんな感じ。ここからname="Redmine"を取り出します。
issues[[1, "project"]]
## $id
## [1] 1
##
## $name
## [1] "Redmine"
後で、purrrを使った処理も説明しますが、割と難しいので、 今は構造を理解するために、とりあえずBase Rで説明します。
NAを考慮しなければ、これでOKです。
issues[[1, "project"]]$name
## [1] "Redmine"
たとえば、categoryとかだとデータにNAが入っていることがあります。 その場合、for文の途中で以下のようなエラーになってしまいます。
tmp_list <- NA
tmp_list$name
## Error in tmp_list$name: $ operator is invalid for atomic vectors
NAじゃないときだけリストの要素(name)にアクセスするように、 ifelseを使って分岐させます。
ifelse(条件, TRUEだったときの値, FALSEだったときの値)
条件には長さが1より大きいかどうかを入れます。
| リスト型のパターン | 長さ |
|---|---|
NAじゃないとき |
length(list(id=12, name="Translations")) = 2 |
NAのとき |
lenght(NA) = 1 |
具体的には、こんな感じです。
category <- list(id=12, name="Translations")
ifelse(length(category) < 2, NA, issues[1, "category"][[1]]$name)
## [1] "Translations"
category <- NA
ifelse(length(category) < 2, NA, issues[1, "category"][[1]]$name)
## [1] NA
ちなみに、is.naを使うと、データフレームがreturnされてしまいます。
is.na(issues[[1, "category"]])
## id name
## FALSE FALSE
このように、created_onが文字列型になってしまっています。
issues[1,"created_on"]
## [1] "2018-10-15T01:43:12Z"
class(issues[1,"created_on"])
## [1] "character"
Redmineのフィールドで日付型は以下の5つ
| フィールド | フィールドの情報 | 例 |
|---|---|---|
| created_on | 日時 | 2018-11-17T10:00:00 |
| closed_on | 日時 | 2018-11-17T12:00:00 |
| updated_on | 日時 | 2018-11-17T15:00:00 |
| start_date | 日付 | 2018-11-17 |
| due_date | 日付 | 2018-11-17 |
文字列 -> 日時型、日付型への変換にはlubridateを使うと便利です。
| 型 | 変換関数 | 変換後の型 |
|---|---|---|
| 日付 | lubridate::ymd |
Date |
| 時間 | lubridate::hms |
Period |
| 日時 | lubridate::ymd_hms |
POSIXct |
日時
date <- lubridate::ymd("2018/11/17")
class(date)
## [1] "Date"
date
## [1] "2018-11-17"
時間
time <- lubridate::hms("01:43:12")
class(time)
## [1] "Period"
## attr(,"package")
## [1] "lubridate"
time
## [1] "1H 43M 12S"
日付
datetime <- lubridate::ymd_hms("2018-10-15T01:43:12Z")
class(datetime)
## [1] "POSIXct" "POSIXt"
datetime
## [1] "2018-10-15 01:43:12 UTC"
tmp_custom_fields <- issues %>%
dplyr::filter(id == 29753) %>% .[[1, "custom_fields"]]
tmp_custom_fields
## [[1]]
## [[1]]$id
## [1] 2
##
## [[1]]$name
## [1] "Resolution"
##
## [[1]]$value
## [1] "Invalid"
##
##
## [[2]]
## [[2]]$id
## [1] 4
##
## [[2]]$name
## [1] "Affected version"
##
## [[2]]$value
## [1] "133"
とっても複雑な構造。。。
要するにこういうこと
list(
list(
list(id=2, name="Resolution", value="Invalid"),
list(id=4, name="Affected version", value=133)
)
)
リスト内の各フィールドの説明
| フィールド | フィールドの説明 |
|---|---|
| id | カスタムフィールドID |
| name | カスタムフィールド名 |
| value | カスタムフィールドの値 |
例えば、Resolutionならこのように取得します。
tmp_custom_fields[[1]]$value
## [1] "Invalid"
Affected versionなら、このように取得します。
tmp_custom_fields[[2]]$value
## [1] "133"
Affected versionのvalueは実際のバージョン名ではなく、各選択肢へのIDになっている。
プルダウンやチェックボックスで選ぶ形式のカスタムフィールドの場合、 valueは各選択肢へのIDになっています。 実際の値を得るためには、バージョンやカスタムフィールドの情報を 別途APIを叩いて入手するしかありません。
以下が、カスタムフィールドの値取得に必要なAPIですが、 残念ながらredmineRでは取れません。 そのため、valueに相当する値を取得するためには、 REST Client(RCurl or httrがメジャーらしい)を使って、 直接REST APIを叩くことになります。
jsonの中身を分析するだけなので、頑張ればできるとは思います。
今回は割愛します。
| フィールドの種類 | API | 必要な権限 |
|---|---|---|
| version型 | /projects/ |
普通でOK |
| list型 | /custom_fields | 管理者権限 |
dplyr使ってできるとことはdplyrで処理します。
df <- issues %>%
dplyr::mutate(
created_on = lubridate::ymd_hms(created_on),
closed_on = lubridate::ymd_hms(closed_on),
updated_on = lubridate::ymd_hms(updated_on),
due_date = lubridate::ymd(due_date),
start_date = lubridate::ymd(start_date)
) %>%
dplyr::select(id, created_on, closed_on, updated_on, due_date, start_date)
df
リスト型はfor文で処理します。(あとでpurrrを使った処理も説明します。)
is_na <- function(.x){
return(length(.x) < 2)
}
for (i in 1:nrow(df)){
# リストの処理
df[i, "project"] <- issues[[i, "project"]]$name
df[i, "tracker"] <- issues[[i, "tracker"]]$name
df[i, "status"] <- issues[[i, "status"]]$name
df[i, "priority"] <- issues[[i, "priority"]]$name
df[i, "author"] <- issues[[i, "author"]]$name
# NAがあるリストの処理
df[i, "category"] <- ifelse(is_na(issues[[i, "category"]]), NA, issues[[i, "category"]]$name)
df[i, "fixed_version"] <- ifelse(is_na(issues[i, "fixed_version"]), NA, issues[i, "fixed_version"][[1]]$name)
df[i, "assigned_to"] <- ifelse(is_na(issues[[i, "assigned_to"]]), NA, issues[[i, "assigned_to"]]$name)
# カスタムフィールドの処理
if(!is_na(issues[[i,"custom_fields"]])){
for(custom_field in issues[[i,"custom_fields"]]){
if(!is.null(custom_field$value)){
df[i, custom_field$name] <- custom_field$value
}
}
}
}
df
for(i in 1:100){
if(is.na(issues[i, "category"])){
if(is.na(issues[[i, "category"]])){
print(i)
}
}
}
## [1] 4
## [1] 9
## [1] 39
## [1] 40
## [1] 50
## [1] 68
## [1] 79
## [1] 87
## [1] 88
#windowsFonts("MEI"=windowsFont("Meiryo"))
df %>% dplyr::select(id, status) %>%
ggplot(aes(x = status)) + geom_bar()
df %>% ggplot(aes(x="", fill=tracker)) +
geom_bar(width = 1) + coord_polar("y") + theme_bw(base_family = "HiraKakuProN-W3")
## Warning in grid.Call(C_textBounds, as.graphicsAnnot(x$label), x$x, x$y, :
## Windows のフォントデータベースにフォントファミリが見付かりません
## Warning in grid.Call(C_textBounds, as.graphicsAnnot(x$label), x$x, x$y, :
## Windows のフォントデータベースにフォントファミリが見付かりません
## Warning in grid.Call(C_textBounds, as.graphicsAnnot(x$label), x$x, x$y, :
## Windows のフォントデータベースにフォントファミリが見付かりません
## Warning in grid.Call(C_textBounds, as.graphicsAnnot(x$label), x$x, x$y, :
## Windows のフォントデータベースにフォントファミリが見付かりません
## Warning in grid.Call(C_textBounds, as.graphicsAnnot(x$label), x$x, x$y, :
## Windows のフォントデータベースにフォントファミリが見付かりません
## Warning in grid.Call(C_textBounds, as.graphicsAnnot(x$label), x$x, x$y, :
## Windows のフォントデータベースにフォントファミリが見付かりません
## Warning in grid.Call(C_textBounds, as.graphicsAnnot(x$label), x$x, x$y, :
## Windows のフォントデータベースにフォントファミリが見付かりません
## Warning in grid.Call.graphics(C_text, as.graphicsAnnot(x$label), x$x, x
## $y, : Windows のフォントデータベースにフォントファミリが見付かりません
## Warning in grid.Call(C_textBounds, as.graphicsAnnot(x$label), x$x, x$y, :
## Windows のフォントデータベースにフォントファミリが見付かりません
## Warning in grid.Call.graphics(C_text, as.graphicsAnnot(x$label), x$x, x
## $y, : Windows のフォントデータベースにフォントファミリが見付かりません
## Warning in grid.Call.graphics(C_text, as.graphicsAnnot(x$label), x$x, x
## $y, : Windows のフォントデータベースにフォントファミリが見付かりません
## Warning in grid.Call.graphics(C_text, as.graphicsAnnot(x$label), x$x, x
## $y, : Windows のフォントデータベースにフォントファミリが見付かりません
open <- df %>%
# created_on(日時)を日付になおす。
mutate(
date = lubridate::as_date(created_on)
) %>%
# dateでグループ化して、dateごとにチケット数を数える。
dplyr::group_by(date) %>%
dplyr::summarise(open = n())
open %>% psych::headTail()
closed_onが入っているチケットは一度クローズされています。 しかし、チケットを再オープンするとclosed_onが入っているのに、クローズされてない状態になります。 そのため、dplyr::filterを使ってクローズチケットのみに絞る必要があります。
close <- df %>%
# クローズチケットだけをカウントする。
dplyr::filter(status=="Closed") %>%
dplyr::mutate(date = lubridate::as_date(closed_on)) %>%
dplyr::group_by(date) %>%
dplyr::summarise(close = n())
close %>% psych::headTail()
最初に起票された日から、 バグが最後に起票された日 or クローズされた日まで、 一日おきの日付を持ったデータフレームを作成します。
min_date <- min(open$date, close$date)
max_date <- max(open$date, close$date)
data.frame(date=seq(min_date, max_date, by = "day")) %>% psych::headTail()
先作った、dateだけを持ったデータフレームにopenとcloseを結合します。 2006-06-29〜2018-10-15の日付情報をすべて残しながら、結合するのでleft_joinを使います。 (このデータならfull_joinでも同じです。)
openclose <- data.frame(date=seq(min_date, max_date, by = "day")) %>%
dplyr::left_join(open, by = "date") %>%
dplyr::left_join(close, by = "date")
openclose
NAが入っているので、0に置き換えます。
openclose <- openclose %>%
replace_na(list(open=0, close=0))
openclose
累積和を求めるためにはcumsumを使います。
openclose$cumsum_open <- cumsum(openclose$open)
openclose$cumsum_close <- cumsum(openclose$close)
openclose %>% head(100)
openclose %>%
ggplot(aes(x = date)) +
geom_line(aes(y = cumsum_close, color = "cumsum_close")) +
geom_line(aes(y = cumsum_open, color = "cumsum_open"))